﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Xml.Linq;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Globalization;

namespace NetLogger
{
	public class NetGeocodeResult
	{
		public NetGeocodeResult( XElement node )
		{
			XElement
				innerNode;
			bool
				hideStreetAddress = false;

			foreach ( XElement item in node.Elements() )
			{
				switch ( item.Name.LocalName )
				{
					case "formatted_address":
						Location = ( string ) item;
						break;
					case "geometry":
						innerNode = item.Element( "location" );
						Latitude = ( double ) innerNode.Element( "lat" );
						Longitude = ( double ) innerNode.Element( "lng" );
						break;
					case "type":
						switch ( ( string ) item )
						{
							case "street_address":
								hideStreetAddress = true;
								IsExact = true;
								break;

							case "intersection":
							case "premise":
							case "subpremise":
							case "neighborhood":
							case "route":
								IsExact = true;
								break;

							default:
								IsExact = false;
								break;
						}
						break;
					case "address_component":
						if ( hideStreetAddress )
						{
							innerNode = item.Element( "type" );
							if ( innerNode != null && ( string ) innerNode == "route" )
							{
								innerNode = item.Element( "long_name" );
								if ( innerNode != null )
									Location = ( string ) innerNode;
							}
						}
						break;
				}
			}
		}

		public NetGeocodeResult( string location )
		{
			Location = location;
		}

		public string Location
		{
			get;
			private set;
		}

		public double Latitude
		{
			get;
			private set;
		}

		public double Longitude
		{
			get;
			private set;
		}

		public bool IsExact
		{
			get;
			private set;
		}

		public override string ToString()
		{
			return Location;
		}
	}

	public class NetLocation : NetPropertyBase, IEquatable<NetLocation>, IEquatable<NetAprs>
	{
		private BackgroundWorker _lvWorker;
		private List<NetGeocodeResult> _lvResults;
		private ObservableCollection<NetGeocodeResult> _uiResults = new ObservableCollection<NetGeocodeResult>();
		private string _originalLocation;
		private string _verifiedLocation;
		private bool _isGps;
		private int _selectedIndex;

		private const double CurrentAge = 10d;
		private const double StaleAge = 30d;
		private const double EarthRadius = 3959d; // average miles

		public enum AprsStatuses
		{
			Undefined,
			Current,
			Stale,
			Old
		}

		public NetLocation()
		{
			_originalLocation = string.Empty;
		}

		public NetLocation( string location )
		{
			_originalLocation = location;
			_lvWorker = new BackgroundWorker();
			_lvResults = new List<NetGeocodeResult>();
			_lvWorker.DoWork += new DoWorkEventHandler( VerifyLocation );
			_lvWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler( VerifyLocationComplete );

			_lvWorker.RunWorkerAsync();
		}

		public NetLocation( NetFormerMember formerMember )
		{
			_originalLocation = formerMember.Location;
			_latitude = formerMember.Latitude;
			_longitude = formerMember.Longitude;
		}

		public NetLocation( XElement node )
			: this()
		{
			foreach ( XElement item in node.Elements() )
			{
				switch ( item.Name.LocalName )
				{
					case "location":
						_originalLocation = NetPost.DecodedString( item );
						_verifiedLocation = null;
						break;
					case "latitude":
						_latitude = ( double ) item;
						break;
					case "longitude":
						_longitude = ( double ) item;
						break;
					case "is_gps":
						_isGps = NetPost.ToBoolean( item );
						break;
					case "aprs_status":
						_aprsStatus = ( AprsStatuses ) ( int ) item;
						break;
				}
			}
		}

		public NetLocation( NetAprs aprsData )
		{
			double
				packetAge;

			_latitude = aprsData.Latitude;
			_longitude = aprsData.Longitude;

			packetAge = ( DateTime.UtcNow - aprsData.UpdateTime ).TotalMinutes;
			if ( packetAge <= CurrentAge )
				_aprsStatus = AprsStatuses.Current;
			else if ( packetAge <= StaleAge )
				_aprsStatus = AprsStatuses.Stale;
			else
				_aprsStatus = AprsStatuses.Old;
		}

		private double _latitude;
		public double Latitude
		{
			get
			{
				return _latitude;
			}
		}

		private double _longitude;
		public double Longitude
		{
			get
			{
				return _longitude;
			}
		}

		private AprsStatuses _aprsStatus;
		public AprsStatuses AprsStatus
		{
			get
			{
				return _aprsStatus;
			}
		}

		public bool IsVerified
		{
			get
			{
				return _latitude != 0d && _longitude != 0d;
			}
		}

		public bool IsDefined
		{
			get
			{
				return !string.IsNullOrWhiteSpace( Location );
			}
		}

		public bool IsGps
		{
			get
			{
				return _isGps;
			}
			set
			{
				_isGps = value;
				OnPropertyChanged( "IsGps" );
			}
		}

		public bool HasGeocodeResults
		{
			get
			{
				return _lvResults != null && _lvResults.Count > 0;
			}
		}

		public ObservableCollection<NetGeocodeResult> GeocodeResults
		{
			get
			{
				return _uiResults;
			}
		}

		public int SelectedIndex
		{
			get
			{
				return _selectedIndex;
			}
			set
			{
				if ( value > 0 )
					NetActivation.Instance.MapProxy.SetTempMarker( _uiResults[ value ].Latitude, _uiResults[ value ].Longitude );
				else
					NetActivation.Instance.MapProxy.ClearTempMarker();

				_selectedIndex = value;
				OnPropertyChanged( "SelectedIndex" );
			}
		}

		public string Location
		{
			get
			{
				switch ( _aprsStatus )
				{
					case AprsStatuses.Current:
						return Properties.Resources.APRSCurrent;
					case AprsStatuses.Stale:
						return Properties.Resources.APRSStale;
					case AprsStatuses.Old:
						return Properties.Resources.APRSOld;
					case AprsStatuses.Undefined:
					default:
						return _isGps ? Properties.Resources.GPS : _verifiedLocation ?? _originalLocation;
				}
			}
		}

		public bool CanUpdateLocation
		{
			get
			{
				return !_isGps && _aprsStatus == AprsStatuses.Undefined;
			}
		}

		public void CommitSelection()
		{
			if ( _selectedIndex >= 0 )
				SetVerifiedLocation( _selectedIndex - 1 );
		}

		public void SetCoordinates( NetMapClickEventArgs e )
		{
			IsGps = false;
			_latitude = e.Latitude;
			_longitude = e.Longitude;
			OnPropertyChanged( "IsVerified" );
			OnPropertyChanged( "Location" );
		}

		public double CalculateDistance( NetLocation other )
		{
			double
				distance,
				a1,
				a2,
				b1,
				b2,
				cosValue;

			if ( IsVerified && other.IsVerified )
			{
				////////////////////////////////////////////////////////////////////////
				// equation from http://www.mathforum.com/library/drmath/view/51711.html
				////////////////////////////////////////////////////////////////////////

				if ( Equals( other ) )
					distance = 0d;
				else
				{
					a1 = Latitude * Math.PI / 180d;
					a2 = other.Latitude * Math.PI / 180d;
					b1 = Longitude * Math.PI / 180d;
					b2 = other.Longitude * Math.PI / 180d;
					cosValue = Math.Cos( a1 ) * Math.Cos( b1 ) * Math.Cos( a2 ) * Math.Cos( b2 ) +
						Math.Cos( a1 ) * Math.Sin( b1 ) * Math.Cos( a2 ) * Math.Sin( b2 ) +
						Math.Sin( a1 ) * Math.Sin( a2 );
					if ( cosValue < -1d )
						cosValue = -1d;
					else if ( cosValue > 1d )
						cosValue = 1d;
					distance = Math.Acos( cosValue ) * EarthRadius;
				}
			}
			else
				distance = double.NaN;

			return distance;
		}

		public override string ToString()
		{
			return Location;
		}

		public override int GetHashCode()
		{
			return _originalLocation.GetHashCode();
		}

		private void SetVerifiedLocation( int index )
		{
			if ( index < 0 )
			{
				_verifiedLocation = null;
				_latitude = 0d;
				_longitude = 0d;
			}
			else
			{
				_verifiedLocation = _lvResults[ index ].Location;
				_latitude = _lvResults[ index ].Latitude;
				_longitude = _lvResults[ index ].Longitude;
			}

			_lvResults.Clear();
			_uiResults.Clear();

			OnPropertyChanged( "IsVerified" );
			OnPropertyChanged( "Location" );
			OnPropertyChanged( "HasGeocodeResults" );
		}

		private void VerifyLocation( object sender, DoWorkEventArgs args )
		{
			Dictionary<string, string>
				query = new Dictionary<string, string>();
			XDocument
				doc;

			query.Add( "address", _originalLocation.Replace( "/", " and " ) + ' ' + NetActivation.GeocodeRegionSuffix );
			query.Add( "sensor", "false" );

			try
			{
				doc = NetActivation.SendWebGetRequest( "http://maps.googleapis.com/maps/api/geocode/xml", query );
				if ( doc != null )
				{
					IEnumerable<XElement> nodes = ( from gr in doc.Elements( "GeocodeResponse" )
													from r in gr.Elements( "result" )
													where ( string ) gr.Element( "status" ) == "OK"
													select r );
					foreach ( XElement item in nodes )
						_lvResults.Add( new NetGeocodeResult( item ) );
				}
			}
			catch
			{
			}
		}

		private void VerifyLocationComplete( object sender, RunWorkerCompletedEventArgs e )
		{
			if ( _lvResults.Count == 1 && _lvResults[ 0 ].IsExact && NetActivation.IsWithinLocationRange( _lvResults[ 0 ].Latitude, _lvResults[ 0 ].Longitude ) )
				SetVerifiedLocation( 0 );
			else if ( !HasGeocodeResults )
				SetVerifiedLocation( -1 );
			else
			{
				_uiResults.Clear();
				_uiResults.Add( new NetGeocodeResult( _originalLocation ) );
				foreach ( NetGeocodeResult item in _lvResults )
					_uiResults.Add( item );
				SelectedIndex = 0;
				OnPropertyChanged( "HasGeocodeResults" );
				OnPropertyChanged( "Location" );
			}
		}

		public bool Equals( NetLocation other )
		{
			return _latitude == other._latitude && _longitude == other._longitude;
		}

		public bool Equals( NetAprs other )
		{
			return _latitude == other.Latitude && _longitude == other.Longitude;
		}
	}
}
